[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください
以下の記事ではcase class
で出来ることを説明しました。
- 基本コンストラクタの引数でメンバーを定義できる
- toStringでインスタンスの内容がわかる
- インスタンスを
==
で比較で - インスタンスをコピー
- インスタンスを作成する際に
new
キーワードが不要 - パターンマッチ
この記事では、case class
で上記が出来る理由を説明します。
基本コンストラクタの引数にはvalがつく
基本コンストラクタでは、以下のように引数を指定することができました。
case class Owner(name: String) case class Book(title: String, owner: Owner)
case class
でない普通のクラスで上記のように定義した場合、「不変なprivateメンバー(private[this]
)」です。
詳細は以下の記事を参照してください。
case class
の基本コンストラクタの引数には、val
がつきます。
そのため、「不変なpublicメンバー」になります。
scala
コマンドを-Xprint:typer
オプションを付けてREPLを起動すると、定義したcase class
がどのように定義されるかがわかります。
実際にREPLを使用してcase class
の基本コンストラクタの引数が実際にどのように定義されるのかを見てみます。
REPLでBook
クラスの基本コンストラクタを定義すると、基本コンストラクタの引数がprivate[this] val
で定義され、値を参照するためアクセッサが、def
で定義されているのがわかります。
<caseaccessor> <paramaccessor> private[this] val title: String = _; <stable> <caseaccessor> <accessor> <paramaccessor> def title: String = Book.this.title; <caseaccessor> <paramaccessor> private[this] val owner: Owner = _; <stable> <caseaccessor> <accessor> <paramaccessor> def owner: Owner = Book.this.owner;
基本コンストラクタの引数は、以下のようにvar
で定義することもできます。
case class Book(title: String, var owner: Owner)
REPLで定義内容を見てみます。
<caseaccessor> <paramaccessor> private[this] val title: String = _; <stable> <caseaccessor> <accessor> <paramaccessor> def title: String = Book.this.title; <caseaccessor> <paramaccessor> private[this] var owner: Owner = _; <caseaccessor> <accessor> <paramaccessor> def owner: Owner = Book.this.owner; <accessor> <paramaccessor> def owner_=(x$1: Owner): Unit = Book.this.owner = x$1;
var
で定義した「owner」に対しては、値を参照するためのアクセッサと値を設定するためのアクセッサが定義されているのがわかります。
そのため、var
を使用した場合は、値を設定するためのアクセッサを使用して値を変更することができます。
val book = Book("Hello World!", Owner("Taro")) book.owner = Owner("Hanako") println(book)
Book(Hello World!,Owner(Hanako))
var
で定義した場合、思わぬ副作用を引き起こす場合がありますので、基本的にはval
を使用するのが良いでしょう。
人が理解できる形式の文字列を返すtoStringメソッドが生成される
case class
のtoString
メソッドは、以下のように人が理解できる形式の文字列を返します。
val book = Book("Hello World!", Owner("Taro")) println(book.toString())
Book(Hello World!,Owner(Taro))
なぜそのような動作をするのかを説明します。
case class
を定義すると、toString
メソッドと他にいくつかのメソッドが追加されます。
まずはcase class
に追加されたtoString
メソッドを見てみてみます。
REPLでOwner
クラスとBook
クラスの定義を確認するとtoString
メソッドは、以下のようにScalaRunTime
の_toString
メソッドを呼び出しているのがわかります。
ScalaRuntime
はScalaのプログラムの実行に必要なサポートメソッドを提供するオブジェクトです。
override <synthetic> def toString(): String = scala.runtime.ScalaRunTime._toString(Book.this);
次にScalaRuntime
の_toString
メソッドを見てみます。
ScalaのソースコードはGithubで公開されています。
Scalaのリポジトリで、ScalaRuntime
のソースコードを確認します。
ScalaRuntime
の_toString
メソッドは、以下のようにcase class
のproductIterator
メソッドが返した値に対して、mkString
メソッドを呼び出して文字列を作成しています。
case class
のproductIterator
は、case class
の基本コンストラクタで指定した値にアクセスするIterator
を返します。
productPrefix
はcase class
の名前を返します。
そのためcase class
のtoString
メソッドは、「クラス名(パラメータの値1, パラメーターの値2, ...)」の形式の文字列を返します。
def _toString(x: Product): String = x.productIterator.mkString(x.productPrefix + "(", ",", ")")
「==」で構造を比較出来るメソッドが生成される
case class
は、Scalaの基本型と同じように==
で構造を比較することができました。
ここではその理由を説明します。
オブジェクトに対して==
を適用すると、以下の記事で説明したようにequals
メソッドが呼び出されます。
case class
を定義すると、コンストラクタの引数で指定した値をすべて比較するequals
メソッドがオーバーライドされます。
そのため、参照先が違っていてもコンストラクタの引数で指定した値がすべて同じであればtrue
になります。
先ほどのREPLで定義したBook
クラスの結果を見るとequals
メソッドは以下のように定義されています。
override <synthetic> def equals(x$1: Any): Boolean = Book.this.eq(x$1.asInstanceOf[Object]).||(x$1 match { case (_: Book) => true case _ => false }.&&({ <synthetic> val Book$1: Book = x$1.asInstanceOf[Book]; Book.this.title.==(Book$1.title).&&(Book.this.owner.==(Book$1.owner)).&&(Book$1.canEqual(Book.this)) }))
最初にeq
メソッドで比較対象のオブジェクトの参照先が同じ場合はtrue
を返します。
次にmatch
式でクラスの型が同じ(Book
)かを確認し、違う場合はfalse
を返します。
最後に基本コンストラクタの引数で渡した値を==
で比較してすべて同じであればtrue
、違う場合はfalse
を返します。
インスタンスをコピーするcopyメソッドが生成される
case class
はcopy
メソッドを使用してインスタンスをコピーすることができました。
case class
を定義するとインスタンスをコピーするcopy
メソッドが生成されます。
先ほどのREPLで定義したBook
クラスの結果を見るとcopy
メソッドは以下のように定義されています。
<synthetic> def copy(title: String = title, owner: Owner = owner): Book = new Book(title, owner);
省略値はコピー元のインスタンスの値になっていますので、引数に指定した値のみ変更してコピーすることができます。
抽出子をもつコンパニオンオブジェクトが生成される
case class
を定義すると、apply
メソッドとunapply
メソッドをもつ「コンパニオンオブジェクト」が生成されます。
これらがどのように作用するのかを説明します。
applyメソッドは、newを指定せずにインスタンスを作成できるようにする
case class
のインスタンスを作成する際にはnew
キーワードは不要でした。
これはcase class
を定義するとapply
メソッドが追加されるためです。
以下のようにBook
のインスタンスを作成するとBook
のapply
メソッドが呼び出されます。
val book = Book("Hello World!", Owner("Taro"))
REPLでBook
を定義した結果を見るとapply
メソッドは以下のように定義されています。
case <synthetic> def apply(title: String, owner: Owner): Book = new Book(title, owner);
apply
メソッドではBook
のインスタンスを作成して返しています。
unapplyメソッドは、パターンマッチを出来るようにする
case class
のインスタンスにパターンマッチを適用して、基本コンストラクタの引数で指定した値を抽出したりmatch
式に適用することができました。
これはcase class
を定義するとunapply
メソッドが追加されるためです。
unapply
メソッドは、apply
メソッドと逆の動きをします。
apply
メソッドは引数からcase class
のインスタンスを作成しましたが、unapply
メソッドはcase class
のインスタンスから基本コンストラクタの引数で指定した値を抽出します。
REPLでBook
を定義した結果を見るとunapply
メソッドは以下のように定義されています。
case <synthetic> def unapply(x$0: Book): Option[(String, Owner)] = if (x$0.==(null)) scala.None else Some.apply[(String, Owner)](scala.Tuple2.apply[String, Owner](x$0.title, x$0.owner));
インスタンスがnull
の場合は、Noneを返します。
null
でない場合は、基本コンストラクタの引数で指定した値のTuple
をSome
でくるんで返します。